﻿==============================================================================
DOSYA ADI  : User.cpp
KAYNAK      : -GameServer\User.cpp
SISTEM      : v2369 Reconnect - SUNUCU
BOYUT       : 224921 bayt / 7311 satir
ACIKLAMA    : Tum reconnect implementasyonu: token, ticket, suspend, confirm, resume, paket filtresi
ESLESME     : 287 satir / 4 blok
==============================================================================

NOT: Bu dosya yalnizca RECONNECT ile ilgili kod bloklarini icerir.
     Satir numaralari KAYNAK dosyadaki orijinal satirlardir.

----------------------------------------------------------------------
>> KAYNAK SATIR 1 - 71
----------------------------------------------------------------------
     1 | #include "stdafx.h"
     2 | #include "KnightsManager.h"
     3 | #include "KingSystem.h"
     4 | #include "MagicInstance.h"
     5 | #include "DBAgent.h"
     6 | #include <algorithm>
     7 | #include "../shared/DateTime.h"
     8 | 
     9 | 
    10 | using namespace std;
    11 | 
    12 | namespace
    13 | {
    14 | 	struct ReconnectTicket
    15 | 	{
    16 | 		std::string account;
    17 | 		std::string character;
    18 | 		ULONGLONG expiresAt;
    19 | 		bool disconnected;
    20 | 		bool genieActive;
    21 | 		int8 merchantState;
    22 | 		uint8 zone;
    23 | 		float x, y, z;
    24 | 		bool merchantValid;
    25 | 		bool premiumMerchant;
    26 | 		_MERCH_DATA merchantItems[MAX_MERCH_ITEMS];
    27 | 		// Parti durumu: disconnect anında üyelik silinmez, ticket'ta saklanır ve
    28 | 		// resume'da yeni oturum parti slotuna yeniden bağlanır.
    29 | 		bool partyValid = false;
    30 | 		int16 partyID = -1;
    31 | 		int8 partySlot = -1;
    32 | 		uint16 partySocket = 0;
    33 | 		bool partyLeader = false;
    34 | 		bool partyCommandLeader = false;
    35 | 		uint8 partyType = 0;
    36 | 	};
    37 | 
    38 | 	std::map<uint32, ReconnectTicket> g_reconnectTickets;
    39 | 	std::recursive_mutex g_reconnectTicketsLock;
    40 | 	volatile LONG g_reconnectSequence = 0;
    41 | 
    42 | 	uint32 CreateReconnectToken(uint16 socketID)
    43 | 	{
    44 | 		uint32 token = static_cast<uint32>(GetTickCount64())
    45 | 			^ (static_cast<uint32>(socketID) << 16)
    46 | 			^ static_cast<uint32>(InterlockedIncrement(&g_reconnectSequence));
    47 | 		return token == 0 ? 1 : token;
    48 | 	}
    49 | }
    50 | 
    51 | #pragma region Constructor
    52 | /**
    53 | * @brief The constructor.
    54 | */
    55 | CUser::CUser(uint16 socketID, SocketMgr* mgr) : KOSocket(socketID, mgr, -1, 1024 * 64, 1024 * 64/*32768, 9172*/), Unit(UnitType::UnitPlayer)
    56 | {
    57 | 	//Initialize();
    58 | }
    59 | #pragma endregion
    60 | 
    61 | #pragma region CUser::OnConnect()
    62 | /**
    63 | * @brief	Executes the connect action.
    64 | */
    65 | void CUser::OnConnect()
    66 | {
    67 | 	KOSocket::OnConnect();
    68 | 	Initialize();
    69 | }
    70 | #pragma endregion
    71 | 

----------------------------------------------------------------------
>> KAYNAK SATIR 378 - 455
----------------------------------------------------------------------
   378 | 
   379 | 	m_tLastTrapAreaTime = 0;
   380 | 	memset(m_iSelMsgEvent, -1, sizeof(m_iSelMsgEvent));
   381 | 	m_sMilitaryTime = 0;
   382 | 	m_switchPremiumCount = 0;
   383 | 	m_sEventNid = m_sEventSid = -1;
   384 | 	m_nQuestHelperID = 0;
   385 | 	m_bZoneChangeFlag = false;
   386 | 	m_bRegeneType = 0;
   387 | 	m_tLastRegeneTime = 0;
   388 | 	m_bZoneChangeSameZone = false;
   389 | 	m_bZoneChangeControl = false;
   390 | 
   391 | 	m_pKnightsUser = nullptr;
   392 | 
   393 | 	m_sRivalID = -1;
   394 | 	m_tRivalExpiryTime = 0;
   395 | 	m_tPlayerEffect = 0;
   396 | 
   397 | 	m_byAngerGauge = 0;
   398 | 
   399 | 	m_bWeaponsDisabled = false;
   400 | 
   401 | 	m_teamColour = TeamColour::TeamColourNone;
   402 | 	m_bGenieStatus = false;
   403 | 	m_reconnectToken = 0;
   404 | 	m_reconnectPendingDisconnectAt = 0;
   405 | 	// Reconnect askı (SUSPENDED) durumu sıfırlanır.
   406 | 	m_reconnectSuspend = CReconnectSuspend();
   407 | 	m_reconnectHydrating = false;
   408 | 	m_reconnectRestoreGenie = false;
   409 | 	m_reconnectMerchantState = MERCHANT_STATE_NONE;
   410 | 	m_reconnectRestoreZone = 0;
   411 | 	m_reconnectRestoreX = 0.0f;
   412 | 	m_reconnectRestoreY = 0.0f;
   413 | 	m_reconnectRestoreZ = 0.0f;
   414 | 	m_reconnectRestoreMerchant = false;
   415 | 	m_reconnectRestorePremiumMerchant = false;
   416 | 	memset(m_reconnectMerchantItems, 0, sizeof(m_reconnectMerchantItems));
   417 | 	// Reconnect parti geri yükleme alanları (disconnect anında ticket'a yazılır,
   418 | 	// karakter yüklemesi sonrası RestoreReconnectParty() ile tüketilir).
   419 | 	m_reconnectRestorePartyValid = false;
   420 | 	m_reconnectRestorePartyID = -1;
   421 | 	m_reconnectRestorePartySlot = -1;
   422 | 	m_reconnectRestorePartySocket = 0;
   423 | 	m_reconnectRestorePartyLeader = false;
   424 | 	m_reconnectRestorePartyCommandLeader = false;
   425 | 	m_reconnectRestorePartyType = 0;
   426 | 	m_reconnectAnchorValid = false;
   427 | 	m_reconnectAnchorZone = 0;
   428 | 	m_reconnectAnchorX = 0.0f;
   429 | 	m_reconnectAnchorY = 0.0f;
   430 | 	m_reconnectAnchorZ = 0.0f;
   431 | 	m_iswanted = false;
   432 | 	m_iswantedtime = 0;
   433 | 	m_bAccountStatus = 0;
   434 | 
   435 | 	// BuhuR Yeni Cz Pvp Kill Ödül Notice Eklendi. 12.04.2025
   436 | 	m_PvpKillCount = 0;
   437 | 	m_PvpKill50Count = 0;
   438 | 	m_PvpKill500Count = 0;
   439 | 	// BuhuR Yeni Cz Pvp Kill Ödül Notice Eklendi. The End. 12.04.2025
   440 | 
   441 | 	for (int i = 0; i < 5; i++)
   442 | 	{
   443 | 		m_bPremiumType[i] = 0;
   444 | 		m_sPremiumTime[i] = 0;
   445 | 	}
   446 | 
   447 | 	m_GameMastersMute = 0;
   448 | 	m_GameMastersUnMute = 0;
   449 | 	m_GameMastersUnBan = 0;
   450 | 	m_GameMastersBanPermit = 0;
   451 | 	m_GameMastersBanUnder = 0;
   452 | 	m_GameMastersBanDays = 0;
   453 | 	m_GameMastersBanCheating = 0;
   454 | 	m_GameMastersBanScamming = 0;
   455 | 	m_GameMastersAllowAttack = 0;

----------------------------------------------------------------------
>> KAYNAK SATIR 503 - 1597
----------------------------------------------------------------------
   503 | 	m_equippedItemBonusLock.lock();
   504 | 	m_equippedItemBonuses.clear();
   505 | 	m_equippedItemBonusLock.unlock();
   506 | 
   507 | 	m_buffLock.lock();
   508 | 	m_buffMap.clear();
   509 | 	m_buffLock.unlock();
   510 | 
   511 | 	m_buff9lock.lock();
   512 | 	m_type9BuffMap.clear();
   513 | 	m_buff9lock.unlock();
   514 | 
   515 | 	m_savedMagicLock.lock();
   516 | 	m_savedMagicMap.clear();
   517 | 	m_savedMagicLock.unlock();
   518 | 
   519 | 	memset(m_sLevelArray, 0, sizeof(m_sLevelArray));
   520 | 	CollectionRace.Initialize();
   521 | 	//m_iteminfo.Initialize();
   522 | }
   523 | #pragma endregion
   524 | 
   525 | #pragma region CUser::OnDisconnect()
   526 | void CUser::OnDisconnect()
   527 | {
   528 | 	CancelReconnectPendingDisconnect();
   529 | 
   530 | 	// Reconnect askı durumu varsa temizle (nesne yeniden kullanılabilir).
   531 | 	ReleaseReconnectSuspend("session closed");
   532 | 
   533 | 	// Resume ticket'ı yalnızca premium oyuncular için hazırlanır: premiumu
   534 | 	// olmayan oyuncu için ticket oluşmaz, dolayısıyla kopmada reconnect
   535 | 	// penceresi açılmaz ve oyuncu normal (doğrudan) disconnect olur.
   536 | 	if (isInGame() && m_reconnectToken != 0 && IsReconnectEligible())
   537 | 		ArmReconnectTicket();
   538 | 
   539 | 	if (isSlaveMerchant())
   540 | 	{
   541 | 		CBot* pSlaveUser = g_pMain->m_MapBotList.GetData(m_bSlaveUserID);
   542 | 		if (pSlaveUser != nullptr) {
   543 | 			GiveSlaveMerchantItems();
   544 | 			pSlaveUser->UserInOut(INOUT_OUT);
   545 | 		}
   546 | 
   547 | 		m_bSlaveMerchant = false;
   548 | 		m_bSlaveUserID = -1;
   549 | 	}
   550 | 
   551 | 	if (g_pMain->pServerSetting.GmisOnlineNotice) // BuhuR Giriş Gm Offline Notice Eklendi 30.12.2024
   552 | 		BuhuRGmisOfflineNotice();
   553 | 
   554 | 	LogOut();
   555 | 	KOSocket::OnDisconnect();
   556 | }
   557 | #pragma endregion
   558 | 
   559 | void CUser::SendReconnectToken()
   560 | {
   561 | 	if (!isInGame())
   562 | 		return;
   563 | 
   564 | 	// Premium kontrolü: reconnect yalnızca premium oyunculara tanınır.
   565 | 	// Premiumu olmayan oyuncuya token gönderilmez; token olmadığı için
   566 | 	// kopmada resume ticket'ı oluşmaz ve askı (SuspendForReconnect) da
   567 | 	// açılamaz. Bu oyuncular bağlantı koptuğunda doğrudan disconnect olur.
   568 | 	if (!IsReconnectEligible())
   569 | 	{
   570 | 		printf("[RECONNECT] token skipped (no premium) account=%s character=%s\n",
   571 | 			GetAccountName().c_str(), GetName().c_str());
   572 | 		return;
   573 | 	}
   574 | 
   575 | 	if (m_reconnectToken == 0)
   576 | 		m_reconnectToken = CreateReconnectToken(GetSocketID());
   577 | 
   578 | 	ReconnectTicket ticket;
   579 | 	ticket.account = GetAccountName();
   580 | 	ticket.character = GetName();
   581 | 	ticket.expiresAt = GetTickCount64() + 60000;
   582 | 	ticket.disconnected = false;
   583 | 	ticket.genieActive = isGenieActive();
   584 | 	ticket.merchantState = GetMerchantState();
   585 | 	ticket.zone = GetZoneID();
   586 | 	ticket.x = GetX();
   587 | 	ticket.y = GetY();
   588 | 	ticket.z = GetZ();
   589 | 	ticket.merchantValid = false;
   590 | 	ticket.premiumMerchant = m_bPremiumMerchant;
   591 | 	memcpy(ticket.merchantItems, m_arMerchantItems, sizeof(ticket.merchantItems));
   592 | 
   593 | 	{
   594 | 		Guard lock(g_reconnectTicketsLock);
   595 | 		g_reconnectTickets[m_reconnectToken] = ticket;
   596 | 	}
   597 | 
   598 | 	Packet result(XSafe);
   599 | 	result << uint8(WIZ_RECONNECT) << uint8(1) << m_reconnectToken;
   600 | 	result.SByte();
   601 | 	result << GetAccountName() << GetName();
   602 | 	Send(&result);
   603 | }
   604 | 
   605 | void CUser::SendReconnectUiTrigger()
   606 | {
   607 | 	if (!isInGame())
   608 | 		return;
   609 | 
   610 | 	// Güvenlik: token dağıtılmadıysa (premium yok) panel tetiği gönderilmez.
   611 | 	// Tokensiz açılan panelde "Yes/No" yanıtları sunucuya iletilemez.
   612 | 	if (m_reconnectToken == 0)
   613 | 		return;
   614 | 
   615 | 	// Client'a "reconnect panelini göster" bildirimi gönder (requestType 5).
   616 | 	// Disconnect ARTIK hemen yapılmaz: panelde kalan süre de iletilir ve
   617 | 	// RECONNECT_TEST_HOLD_SECONDS boyunca karakter oyunda askıda kalır.
   618 | 	// Süre içinde client "oyunda kal" (requestType 6) gönderirse disconnect
   619 | 	// iptal edilir; süre dolarsa kullanıcı düşürülür ve resume akışı devreye girer.
   620 | 	Packet result(XSafe);
   621 | 	result << uint8(WIZ_RECONNECT) << uint8(5) << uint8(RECONNECT_TEST_HOLD_SECONDS);
   622 | 	Send(&result);
   623 | }
   624 | 
   625 | void CUser::SendReconnectDisconnectNotice()
   626 | {
   627 | 	if (!isInGame())
   628 | 		return;
   629 | 
   630 | 	// Client'a "disconnect şimdi uygulanıyor" bildirimi (requestType 9).
   631 | 	// Client bu bildirimle köprüyü devre dışı bırakır; oyunun kendi disconnect
   632 | 	// paneli normal şekilde açılır ve üstünde yeniden bağlanma paneli kalır.
   633 | 	Packet result(XSafe);
   634 | 	result << uint8(WIZ_RECONNECT) << uint8(9);
   635 | 	Send(&result);
   636 | }
   637 | 
   638 | void CUser::ArmReconnectTicket()
   639 | {
   640 | 	Guard lock(g_reconnectTicketsLock);
   641 | 
   642 | 	// Premium kapısı (krallık kuralı): premiumsuz oyuncu için ticket YOK.
   643 | 	// m_reconnectToken, SendReconnectToken()'daki gate sayesinde premiumsuzda
   644 | 	// zaten 0'dır; bu ek kontrol yarış durumlarına (resume yoldayken premium
   645 | 	// bitimi vb.) karşı ikinci kilittir. Token üretimi ayrıca yalnızca
   646 | 	// SendReconnectToken() yapar; burada asla token üretilmez.
   647 | 	if (!IsReconnectEligible() || m_reconnectToken == 0)
   648 | 		return;
   649 | 
   650 | 	// Ticket, RESUME kabul edilince silinir. Tekrarlanan kopmalarda (2., 3. ...)
   651 | 	// resume akışının çalışmaya devam etmesi için ticket yoksa yeniden
   652 | 	// oluşturulur; varsa güncellenir. Her kopmada güncel konum/Genie/Pazar
   653 | 	// anlık görüntüsü ticket'a yazılır.
   654 | 	ReconnectTicket& ticket = g_reconnectTickets[m_reconnectToken];
   655 | 	ticket.account = GetAccountName();
   656 | 	ticket.character = GetName();
   657 | 	ticket.expiresAt = GetTickCount64() + 60000;
   658 | 	ticket.disconnected = true;
   659 | 	ticket.genieActive = isGenieActive();
   660 | 	ticket.merchantState = GetMerchantState();
   661 | 	ticket.zone = GetZoneID();
   662 | 	ticket.x = GetX();
   663 | 	ticket.y = GetY();
   664 | 	ticket.z = GetZ();
   665 | 	ticket.merchantValid = isMerchanting() || isSellingMerchantingPreparing() || isBuyingMerchantingPreparing();
   666 | 	ticket.premiumMerchant = m_bPremiumMerchant;
   667 | 	memcpy(ticket.merchantItems, m_arMerchantItems, sizeof(ticket.merchantItems));
   668 | 
   669 | 	// Parti snapshot'i: disconnect sırasında üyelik SİLİNMEYECEK (bkz.
   670 | 	// ReqUserLogOut / PartyNemberRemove reconnect koruması); resume'da bu
   671 | 	// bilgiyle yeni oturum parti slotuna yeniden bağlanır.
   672 | 	ticket.partyValid = false;
   673 | 	ticket.partyID = -1;
   674 | 	ticket.partySlot = -1;
   675 | 	ticket.partySocket = 0;
   676 | 	ticket.partyLeader = false;
   677 | 	ticket.partyCommandLeader = false;
   678 | 	ticket.partyType = 0;
   679 | 	if (isInParty())
   680 | 	{
   681 | 		_PARTY_GROUP* pParty = g_pMain->GetPartyPtr(GetPartyID());
   682 | 		if (pParty != nullptr)
   683 | 		{
   684 | 			for (int i = 0; i < MAX_PARTY_USERS; i++)
   685 | 			{
   686 | 				if (pParty->uid[i] != GetSocketID())
   687 | 					continue;
   688 | 
   689 | 				ticket.partyValid = true;
   690 | 				ticket.partyID = GetPartyID();
   691 | 				ticket.partySlot = (int8)i;
   692 | 				ticket.partySocket = (uint16)GetSocketID();
   693 | 				ticket.partyLeader = m_bPartyLeader;
   694 | 				ticket.partyCommandLeader = m_bPartyCommandLeader;
   695 | 				ticket.partyType = m_sUserPartyType;
   696 | 				break;
   697 | 			}
   698 | 		}
   699 | 	}
   700 | }
   701 | 
   702 | void CUser::ArmReconnectPendingDisconnect(uint32 holdSeconds)
   703 | {
   704 | 	Guard lock(g_reconnectTicketsLock);
   705 | 	m_reconnectPendingDisconnectAt = GetTickCount64() + (ULONGLONG)holdSeconds * 1000;
   706 | }
   707 | 
   708 | void CUser::CancelReconnectPendingDisconnect()
   709 | {
   710 | 	Guard lock(g_reconnectTicketsLock);
   711 | 	m_reconnectPendingDisconnectAt = 0;
   712 | }
   713 | 
   714 | bool CUser::ProcessReconnectPendingDisconnect()
   715 | {
   716 | 	ULONGLONG deadline = 0;
   717 | 	{
   718 | 		Guard lock(g_reconnectTicketsLock);
   719 | 		deadline = m_reconnectPendingDisconnectAt;
   720 | 	}
   721 | 
   722 | 	if (deadline == 0 || GetTickCount64() < deadline)
   723 | 		return false;
   724 | 
   725 | 	{
   726 | 		Guard lock(g_reconnectTicketsLock);
   727 | 		m_reconnectPendingDisconnectAt = 0;
   728 | 	}
   729 | 
   730 | 	printf("[RECONNECT] hold expired, disconnecting account=%s character=%s\n",
   731 | 		GetAccountName().c_str(), GetName().c_str());
   732 | 
   733 | 	// Disconnect'ten ÖNCE bildir: client köprüyü devre dışı bırakıp normal
   734 | 	// disconnect panelini göstersin.
   735 | 	SendReconnectDisconnectNotice();
   736 | 
   737 | 	ArmReconnectTicket();
   738 | 	goDisconnect("reconnect test hold expired", __FUNCTION__);
   739 | 	return true;
   740 | }
   741 | 
   742 | #pragma region Reconnect / SUSPENDED durum yönetimi
   743 | // ---------------------------------------------------------------------------
   744 | //  Askı (SUSPENDED) sisteminin CUser tarafı: durum geçişleri, güvenlik
   745 | //  doğrulaması ve paket filtresi. Durum çekirdeği CReconnectSuspend'tedir.
   746 | // ---------------------------------------------------------------------------
   747 | 
   748 | ReconnectIdentity CUser::BuildReconnectIdentity()
   749 | {
   750 | 	ReconnectIdentity identity;
   751 | 	identity.accountID = GetAccountName();
   752 | 	identity.characterName = GetName();
   753 | 	identity.characterID = GetID();
   754 | 	identity.socketID = GetSocketID();
   755 | 	identity.token = m_reconnectToken;
   756 | 	return identity;
   757 | }
   758 | 
   759 | bool CUser::SuspendForReconnect(uint8 holdSeconds, const char* reason)
   760 | {
   761 | 	if (!isInGame())
   762 | 		return false;
   763 | 
   764 | 	// Zaten askıda olan bir oturum tekrar askıya alınamaz.
   765 | 	if (GetState() == GameState::GAME_STATE_SUSPENDED)
   766 | 		return false;
   767 | 
   768 | 	// Premium kontrolü: reconnect hizmeti yalnızca premium oyunculara açıktır.
   769 | 	// Premiumu olmayan oyuncu askıya alınmaz; panel açılmaz ve kopmada doğrudan
   770 | 	// disconnect olur. Bu kontrol GM'ler dahil herkes için geçerlidir; gerçek
   771 | 	// kopma yolu (OnDisconnect -> ArmReconnectTicket) ile aynı katı premium
   772 | 	// kuralı uygulanır.
   773 | 	if (!IsReconnectEligible())
   774 | 	{
   775 | 		printf("[RECONNECT] SUSPEND rejected (premium required) account=%s character=%s\n",
   776 | 			GetAccountName().c_str(), GetName().c_str());
   777 | 		return false;
   778 | 	}
   779 | 
   780 | 	// Token + ticket dağıtımı: yeniden bağlanma ve kimlik doğrulaması için
   781 | 	// gereklidir. Her askıda yeniden üretilir; çünkü başarılı bir resume
   782 | 	// sonrası ticket haritadan silinmiş olabilir.
   783 | 	SendReconnectToken();
   784 | 
   785 | 	const ReconnectIdentity identity = BuildReconnectIdentity();
   786 | 	if (!identity.IsUsable() || !m_reconnectSuspend.Begin(holdSeconds, identity))
   787 | 		return false;
   788 | 
   789 | 	// Bekleyen planlı disconnect varsa iptal et: karar artık panelden gelecek.
   790 | 	CancelReconnectPendingDisconnect();
   791 | 
   792 | 	// Durum geçişi: IN_GAME -> SUSPENDED
   793 | 	// (isInGame() SUSPENDED'ı da kapsadığı için karakter bölgede kalır.)
   794 | 	m_state = GameState::GAME_STATE_SUSPENDED;
   795 | 
   796 | 	// --- Panel konumu sabitlenir (HOLD anchor) ----------------------------
   797 | 	// Panelin açıldığı andaki konum kaydedilir. Askı boyunca sunucu bu
   798 | 	// noktada donuk kalır; istemci yerel olarak yürüyebilir. "Yes" onayında
   799 | 	// HandleReconnectConfirm karakteri tam bu koordinata ışınlar — böylece
   800 | 	// askı sırasında yapılan yerel yürüyüş iptal edilir ve karakter panelin
   801 | 	// açıldığı noktadan oyuna devam eder.
   802 | 	m_reconnectAnchorZone = GetZoneID();
   803 | 	m_reconnectAnchorX = GetX();
   804 | 	m_reconnectAnchorY = GetY();
   805 | 	m_reconnectAnchorZ = GetZ();
   806 | 	m_reconnectAnchorValid = true;
   807 | 	// -----------------------------------------------------------------------
   808 | 
   809 | 	// --- Hareket (yürüme) durumunu askıya al -------------------------------
   810 | 	// Askı boyunca WIZ_MOVE paketleri yutulduğu için istemcilere "finish"
   811 | 	// (dur) paketi VARMAYACAK: yakındaki istemcilerde karakterin yürüme
   812 | 	// animasyonu askı boyunca aktif kalırdı. Bölgeye hız 0 + echo=finish
   813 | 	// yayını yapılarak yürüme eylemi durdurulur; yerel hareket takibi
   814 | 	// (pMove echo/speed geçmişi ve anlık hız) de sıfırlanır.
   815 | 	pMove.Initialize();
   816 | 	m_sSpeed = 0;
   817 | 	{
   818 | 		const uint16 stopX = (uint16)(GetX() * 10.0f);
   819 | 		const uint16 stopZ = (uint16)(GetZ() * 10.0f);
   820 | 		const uint16 stopY = (uint16)(GetY() * 10.0f);
   821 | 		Packet stopMove(WIZ_MOVE);
   822 | 		stopMove << GetSocketID() << stopX << stopZ << stopY
   823 | 			<< int16(0) << uint8(moveop::finish);
   824 | 		g_pMain->Send_NearRegion(&stopMove, GetMap(), GetRegionX(), GetRegionZ(),
   825 | 			m_curx, m_curz, this, GetEventRoom());
   826 | 
   827 | 		// Bölge yayını KENDİ istemciyi atlar (Send_NearRegion içinde
   828 | 		// pUser == pExceptUser -> continue). Bu yüzden finish paketi
   829 | 		// ayrıca DOĞRUDAN bu oyuncuya gönderilir: istemcinin kendi
   830 | 		// WIZ_MOVE işleyicisi (sub_81F730, self dalı: chr+0x7D8/7DC/7E0
   831 | 		// alanlarına mevcut koordinatlar yazılır) yerel yürüyüşü
   832 | 		// durdurur. Böylece 1. /reconnecttest çağrısında yürüme eylemi
   833 | 		// iptal edilir; Yes onayındaki anchor warp ile de desteklenir.
   834 | 		Send(&stopMove);
   835 | 	}
   836 | 	// -----------------------------------------------------------------------
   837 | 
   838 | 	// Askı ANINDA karakter durumu kaydedilir (Genie, pazar durumu, bölge ve
   839 | 	// konum). Böylece hangi sebeple olursa olsun bir sonraki adımda disconnect
   840 | 	// gerçekleşse bile resume akışı askı anındaki son durumla çalışır.
   841 | 	ArmReconnectTicket();
   842 | 
   843 | 	// İstemciye "UI panelini aç" paketini gönder (requestType 5 + süre).
   844 | 	SendReconnectUiTrigger();
   845 | 
   846 | 	printf("[RECONNECT] SUSPENDED account=%s character=%s id=%u socket=%u token=%u hold=%us reason=%s\n",
   847 | 		identity.accountID.c_str(), identity.characterName.c_str(), identity.characterID,
   848 | 		identity.socketID, identity.token, (uint32)holdSeconds,
   849 | 		reason != nullptr ? reason : "-");
   850 | 	return true;
   851 | }
   852 | 
   853 | void CUser::ReleaseReconnectSuspend(const char* reason)
   854 | {
   855 | 	if (!m_reconnectSuspend.IsActive() && GetState() != GameState::GAME_STATE_SUSPENDED)
   856 | 		return;
   857 | 
   858 | 	m_reconnectSuspend.Release(reason);
   859 | 
   860 | 	// Askıdan çıkan karakter tekrar aktif oyuncu olur.
   861 | 	if (m_state == GameState::GAME_STATE_SUSPENDED)
   862 | 		m_state = GameState::GAME_STATE_INGAME;
   863 | 
   864 | 	printf("[RECONNECT] SUSPENDED released account=%s character=%s reason=%s\n",
   865 | 		GetAccountName().c_str(), GetName().c_str(),
   866 | 		reason != nullptr ? reason : "-");
   867 | }
   868 | 
   869 | // Reconnect akışı sürüyor mu?
   870 | //  - askı (SUSPENDED) aktif, ya da
   871 | //  - disconnect edilmiş reconnect ticket'ı hâlâ geçerli (yeniden bağlanma
   872 | //    bekleniyor). Parti üyeliği gibi oturum durumları bu pencerede korunur.
   873 | bool CUser::IsReconnectPending()
   874 | {
   875 | 	if (m_reconnectSuspend.IsActive())
   876 | 		return true;
   877 | 
   878 | 	if (m_reconnectToken == 0)
   879 | 		return false;
   880 | 
   881 | 	Guard lock(g_reconnectTicketsLock);
   882 | 	auto it = g_reconnectTickets.find(m_reconnectToken);
   883 | 	if (it == g_reconnectTickets.end())
   884 | 		return false;
   885 | 
   886 | 	return it->second.disconnected && it->second.expiresAt >= GetTickCount64();
   887 | }
   888 | 
   889 | // Reconnect (yeniden bağlanma) hakkı yalnızca premium oyunculara tanınır.
   890 | // Premium türü aktif değilse (NO_PREMIUM) token dağıtımı, askı paneli ve
   891 | // resume akışının tamamı devre dışıdır; kopmada oyuncu doğrudan düşer.
   892 | bool CUser::IsReconnectEligible() const
   893 | {
   894 | 	return m_bPremiumInUse != NO_PREMIUM;
   895 | }
   896 | 
   897 | // Reconnect hakkını iptal eder: token/ticket silinir ve istemciye
   898 | // RECONNECT_REQ_REVOKE bildirimi gönderilir (AC tarafı reconnectToken'ı
   899 | // temizler; köprü/panel bir daha başlamaz, kopmada doğrudan disconnect olur).
   900 | void CUser::SendReconnectRevoke()
   901 | {
   902 | 	if (m_reconnectToken != 0)
   903 | 	{
   904 | 		Guard lock(g_reconnectTicketsLock);
   905 | 		g_reconnectTickets.erase(m_reconnectToken);
   906 | 		m_reconnectToken = 0;
   907 | 	}
   908 | 
   909 | 	printf("[RECONNECT] REVOKED account=%s character=%s\n",
   910 | 		GetAccountName().c_str(), GetName().c_str());
   911 | 
   912 | 	if (!isInGame())
   913 | 		return;
   914 | 
   915 | 	Packet result(XSafe);
   916 | 	result << uint8(WIZ_RECONNECT) << uint8(RECONNECT_REQ_REVOKE);
   917 | 	Send(&result);
   918 | }
   919 | 
   920 | // Premium durumu değişiminde reconnect hakkını senkronlar:
   921 | //  - Premium aktifleştiyse hemen token dağıtılır (sonradan premium alan
   922 | //    oyuncu da reconnect kullanabilir).
   923 | //  - Premium düştüyse hak iptal edilir; karakter askıdaysa askı kapatılıp
   924 | //    doğrudan disconnect uygulanır (panel beklemeden, anında düşer).
   925 | void CUser::SyncReconnectEligibility()
   926 | {
   927 | 	if (IsReconnectEligible())
   928 | 	{
   929 | 		SendReconnectToken();
   930 | 		return;
   931 | 	}
   932 | 
   933 | 	const bool suspended = (GetState() == GameState::GAME_STATE_SUSPENDED)
   934 | 		|| m_reconnectSuspend.IsActive();
   935 | 
   936 | 	if (suspended)
   937 | 	{
   938 | 		// Askıyı kapat (durum IN_GAME'e döner), hakkı iptal et ve
   939 | 		// disconnect'i hemen uygula — kullanıcının istediği gibi
   940 | 		// panel/Yes beklemeden doğrudan disconnect.
   941 | 		ReleaseReconnectSuspend("reconnect revoked (no premium)");
   942 | 		SendReconnectRevoke();
   943 | 		SendReconnectDisconnectNotice();
   944 | 		goDisconnect("reconnect revoked: premium expired", __FUNCTION__);
   945 | 		return;
   946 | 	}
   947 | 
   948 | 	SendReconnectRevoke();
   949 | }
   950 | #pragma endregion
   951 | 
   952 | bool CUser::HandleReconnectConfirm(Packet& pkt, uint8 requestType)
   953 | {
   954 | 	// Gövde: [requestType] [token] SByte [account] [character]
   955 | 	//    (+ requestType == RECONNECT_REQ_CONFIRM_WITH_ID ise [characterID])
   956 | 	//
   957 | 	// NOT: XSafe_HandlePacket requestType'ı yalnızca bir *kopya* paketten okur
   958 | 	// (Packet probe = pkt); gerçek pkt imleci hâlâ requestType baytındadır.
   959 | 	// Burada requestType'ı tüketip imleci token'dan itibareye getiriyoruz,
   960 | 	// yoksa token garbage okunur, güvenlik mismatch → istemcide kopma paneli çıkar.
   961 | 	uint8 receivedRequestType = 0;
   962 | 	pkt >> receivedRequestType;
   963 | 	bool requestTypeOk = (receivedRequestType == requestType)
   964 | 		|| (requestType == RECONNECT_REQ_CONFIRM && receivedRequestType == RECONNECT_REQ_CONFIRM_WITH_ID)
   965 | 		|| (requestType == RECONNECT_REQ_CONFIRM_WITH_ID && receivedRequestType == RECONNECT_REQ_CONFIRM);
   966 | 	if (!requestTypeOk)
   967 | 		return false;
   968 | 
   969 | 	uint32 token = 0;
   970 | 	std::string account, character;
   971 | 	uint16 characterID = 0;
   972 | 
   973 | 		pkt >> token;
   974 | 	pkt.SByte();
   975 | 	pkt >> account >> character;
   976 | 	if (requestType == RECONNECT_REQ_CONFIRM_WITH_ID && pkt.size() > 0)
   977 | 		pkt >> characterID;
   978 | 
   979 | 	// Paket iki kanaldan gelebilir:
   980 | 	//  1) Askıdaki karakterin kendi oyun soketinden (normal yol),
   981 | 	//  2) UI panelinin açtığı transport (köprü) soketinden. Transport soketi
   982 | 	//     sunucuda AYRI bir CUser oluşturur; bu CUser askıda olmadığı için
   983 | 	//     onay, askıdaki gerçek kullanıcıya token üzerinden bağlanır.
   984 | 	CUser* pTarget = this;
   985 | 	bool transportPath = false;
   986 | 
   987 | 	if (GetState() != GameState::GAME_STATE_SUSPENDED || !m_reconnectSuspend.IsActive())
   988 | 	{
   989 | 		CUser* pSuspended = nullptr;
   990 | 		{
   991 | 			Guard lock(g_reconnectTicketsLock);
   992 | 			auto it = g_reconnectTickets.find(token);
   993 | 			if (it != g_reconnectTickets.end())
   994 | 			{
   995 | 				pSuspended = g_pMain->GetUserPtr(it->second.account, NameType::TYPE_ACCOUNT);
   996 | 				if (pSuspended == nullptr && !it->second.character.empty())
   997 | 					pSuspended = g_pMain->GetUserPtr(it->second.character, NameType::TYPE_CHARACTER);
   998 | 			}
   999 | 		}
  1000 | 
  1001 | 		if (pSuspended != nullptr && pSuspended != this
  1002 | 			&& pSuspended->GetState() == GameState::GAME_STATE_SUSPENDED
  1003 | 			&& pSuspended->m_reconnectSuspend.IsActive()
  1004 | 			&& pSuspended->m_reconnectSuspend.Token() == token)
  1005 | 		{
  1006 | 			pTarget = pSuspended;
  1007 | 			transportPath = true;
  1008 | 		}
  1009 | 		else
  1010 | 		{
  1011 | 			// Aktif askı yoksa: planlı test disconnect'ini iptal etmeyi dene,
  1012 | 			// istemciye başarısız ack'i gönder.
  1013 | 			CancelReconnectPendingDisconnect();
  1014 | 
  1015 | 			Packet ack(XSafe);
  1016 | 			ack << uint8(WIZ_RECONNECT) << uint8(RECONNECT_REQ_CONFIRM_ACK) << uint8(0);
  1017 | 			Send(&ack);
  1018 | 
  1019 | 			printf("[RECONNECT] CONFIRM ignored (no active suspend) account=%s character=%s\n",
  1020 | 				account.c_str(), character.c_str());
  1021 | 			return false;
  1022 | 		}
  1023 | 	}
  1024 | 
  1025 | 	// --- GÜVENLİK DOĞRULAMASI -------------------------------------------
  1026 | 	// Paketin geldiği oturumun bilgileriyle karşılaştırılır: token, hesap adı,
  1027 | 	// karakter adı, karakter ID ve oturum (socket) ID eşleşmelidir.
  1028 | 	ReconnectIdentity incoming;
  1029 | 	incoming.accountID = account;
  1030 | 	incoming.characterName = character;
  1031 | 	incoming.characterID = characterID;
  1032 | 	// Transport yolunda paket farklı bir soketten geldiği için soket
  1033 | 	// eşleşmesi atlanır (token + hesap + karakter doğrulaması uygulanır).
  1034 | 	incoming.socketID = transportPath ? 0 : pTarget->GetSocketID();
  1035 | 	incoming.token = token;
  1036 | 
  1037 | 	std::string failReason;
  1038 | 	const Reconnect::SuspendResult result = pTarget->m_reconnectSuspend.Confirm(incoming, failReason);
  1039 | 
  1040 | 	if (result != Reconnect::SuspendResult::Confirmed)
  1041 | 	{
  1042 | 		printf("[RECONNECT] CONFIRM REJECTED account=%s character=%s result=%s detail=%s attempts=%u\n",
  1043 | 			account.c_str(), character.c_str(), Reconnect::ToString(result), failReason.c_str(),
  1044 | 			pTarget->m_reconnectSuspend.ConfirmAttempts());
  1045 | 
  1046 | 		// Süre dolduysa: askıyı kapat ve normal disconnect akışına geç.
  1047 | 		if (result == Reconnect::SuspendResult::Expired)
  1048 | 		{
  1049 | 			// Transport soketi ack bekliyor; çıkmadan önce bildir.
  1050 | 			Packet ack(XSafe);
  1051 | 			ack << uint8(WIZ_RECONNECT) << uint8(RECONNECT_REQ_CONFIRM_ACK) << uint8(0);
  1052 | 			Send(&ack);
  1053 | 
  1054 | 			pTarget->ReleaseReconnectSuspend("confirm after expiry");
  1055 | 			pTarget->ArmReconnectTicket();
  1056 | 			pTarget->SendReconnectDisconnectNotice();
  1057 | 			pTarget->goDisconnect("reconnect confirm expired", __FUNCTION__);
  1058 | 			return false;
  1059 | 		}
  1060 | 
  1061 | 		// Kimlik/oturum uyuşmazlığı: saldırı girişimi, oturumu sonlandır.
  1062 | 		switch (result)
  1063 | 		{
  1064 | 		case Reconnect::SuspendResult::TokenMismatch:
  1065 | 		case Reconnect::SuspendResult::SessionMismatch:
  1066 | 		case Reconnect::SuspendResult::AccountMismatch:
  1067 | 		case Reconnect::SuspendResult::CharacterIDMismatch:
  1068 | 			pTarget->goDisconnect("reconnect confirm security violation", __FUNCTION__);
  1069 | 			return false;
  1070 | 		default:
  1071 | 			break;
  1072 | 		}
  1073 | 
  1074 | 		if (pTarget->m_reconnectSuspend.ConfirmAttempts() >= RECONNECT_MAX_CONFIRM_ATTEMPTS)
  1075 | 		{
  1076 | 			pTarget->ReleaseReconnectSuspend("too many confirm attempts");
  1077 | 			pTarget->goDisconnect("reconnect confirm abuse", __FUNCTION__);
  1078 | 		}
  1079 | 		return false;
  1080 | 	}
  1081 | 
  1082 | 	// --- Doğrulama başarılı: SUSPENDED -> IN_GAME ------------------------
  1083 | 	// (Askı CReconnectSuspend::Confirm içinde "confirmed" ile bırakıldı.)
  1084 | 	pTarget->m_state = GameState::GAME_STATE_INGAME;
  1085 | 	pTarget->CancelReconnectPendingDisconnect();
  1086 | 
  1087 | 	// --- "Yes" → panel konumuna ışınla (HOLD anchor) ----------------------
  1088 | 	// Panelin açıldığı andaki sabit koordinata teleport: askı boyunca sunucu
  1089 | 	// konumu donuk kalırken istemci yerel olarak yürüyebilir; bu Warp akışı
  1090 | 	// (WIZ_WARP + SetPosition + region/görünürlük yenileme) yerel yürüyüşü
  1091 | 	// iptal ederek karakteri tam olarak panelin açıldığı noktaya döndürür.
  1092 | 	// Zone askı sırasında sunucu tarafında değişmişse dokunulmaz (yeni zone
  1093 | 	// geçerlidir). m_bWarp yalnızca bekleyen zone-change akışında true olur;
  1094 | 	// o durumda Warp() kendiliğinden atlanır ve mevcut akış konumu yönetir.
  1095 | 	if (pTarget->m_reconnectAnchorValid)
  1096 | 	{
  1097 | 		pTarget->m_reconnectAnchorValid = false;
  1098 | 		if (pTarget->GetZoneID() == pTarget->m_reconnectAnchorZone)
  1099 | 		{
  1100 | 			pTarget->SetPosition(pTarget->m_reconnectAnchorX, pTarget->m_reconnectAnchorY, pTarget->m_reconnectAnchorZ);
  1101 | 			pTarget->Warp(uint16(pTarget->m_reconnectAnchorX * 10.0f),
  1102 | 				uint16(pTarget->m_reconnectAnchorZ * 10.0f));
  1103 | 			printf("[RECONNECT] Yes -> anchor teleport account=%s character=%s zone=%u pos=(%.1f, %.1f, %.1f)\n",
  1104 | 				pTarget->GetAccountName().c_str(), pTarget->GetName().c_str(),
  1105 | 				(unsigned)pTarget->GetZoneID(),
  1106 | 				pTarget->GetX(), pTarget->GetY(), pTarget->GetZ());
  1107 | 		}
  1108 | 	}
  1109 | 	// -----------------------------------------------------------------------
  1110 | 
  1111 | 	// İstemciye onay sonucunu bildir (panel kapanır, oyun kaldığı yerden sürer).
  1112 | 	// - transport yolunda: ack hem transport soketine (client bekliyor) hem
  1113 | 	//   oyun soketine (panel/geri sayım temizliği) gönderilir.
  1114 | 	Packet ack(XSafe);
  1115 | 	ack << uint8(WIZ_RECONNECT) << uint8(RECONNECT_REQ_CONFIRM_ACK) << uint8(1);
  1116 | 	Send(&ack);
  1117 | 	if (transportPath)
  1118 | 		pTarget->Send(&ack);
  1119 | 
  1120 | 	printf("[RECONNECT] CONFIRMED (IN_GAME restored) account=%s character=%s id=%u token=%u%s\n",
  1121 | 		account.c_str(), character.c_str(), pTarget->GetID(), token,
  1122 | 		transportPath ? " [transport]" : "");
  1123 | 	return true;
  1124 | }
  1125 | 
  1126 | bool CUser::UpdateReconnectSuspend()
  1127 | {
  1128 | 	if (GetState() != GameState::GAME_STATE_SUSPENDED || !m_reconnectSuspend.IsActive())
  1129 | 		return false;
  1130 | 
  1131 | 	if (!m_reconnectSuspend.IsExpired())
  1132 | 		return false;
  1133 | 
  1134 | 	// Süre doldu: askıyı kapat, durumu IN_GAME'e al ve karakteri düşür.
  1135 | 	printf("[RECONNECT] SUSPENDED expired account=%s character=%s held=%us\n",
  1136 | 		GetAccountName().c_str(), GetName().c_str(), m_reconnectSuspend.HoldSeconds());
  1137 | 
  1138 | 	ReleaseReconnectSuspend("hold expired");
  1139 | 
  1140 | 	// İstemci yeniden bağlanabilsin diye ticket hazırlanır ve bildirilir.
  1141 | 	ArmReconnectTicket();
  1142 | 	SendReconnectDisconnectNotice();
  1143 | 	goDisconnect("reconnect suspend expired", __FUNCTION__);
  1144 | 	return true;
  1145 | }
  1146 | 
  1147 | bool CUser::IsReconnectPacketAllowed(uint8 opcode, Packet& pkt)
  1148 | {
  1149 | 	// Askı aktif değilse filtre uygulanmaz.
  1150 | 	if (!m_reconnectSuspend.IsActive())
  1151 | 		return true;
  1152 | 
  1153 | 	if (!CReconnectSuspend::IsAllowedOpcode(opcode))
  1154 | 		return false;
  1155 | 
  1156 | 	// XSafe kanalında yalnızca izinli alt opcode'lar geçer (onay paketi vb.).
  1157 | 	if (opcode == XSafe)
  1158 | 	{
  1159 | 		// Paketin okuma imlecini bozmamak için kopyası üzerinden bakılır.
  1160 | 		Packet probe = pkt;
  1161 | 		uint8 subOpcode = 0;
  1162 | 		probe >> subOpcode;
  1163 | 		return CReconnectSuspend::IsAllowedXSafeSubOpcode(subOpcode);
  1164 | 	}
  1165 | 
  1166 | 	return true;
  1167 | }
  1168 | 
  1169 | void CUser::HandleReconnectResume(Packet& pkt)
  1170 | {
  1171 | 	uint8 requestType = 0;
  1172 | 	uint32 token = 0;
  1173 | 	std::string account, character;
  1174 | 
  1175 | 	pkt >> requestType;
  1176 | 	if (requestType == 4)
  1177 | 	{
  1178 | 		uint8 status = 0;
  1179 | 		pkt >> status;
  1180 | 		printf("[RECONNECT] transport status=%u account=%s character=%s\n",
  1181 | 			status, GetAccountName().c_str(), GetName().c_str());
  1182 | 		return;
  1183 | 	}
  1184 | 	if (requestType == 6)
  1185 | 	{
  1186 | 		// "Oyunda kal": planlı reconnect test disconnect'ini iptal et.
  1187 | 		pkt >> token;
  1188 | 		pkt.SByte();
  1189 | 		pkt >> account >> character;
  1190 | 		printf("[RECONNECT] STAY received account=%s character=%s token=%u\n",
  1191 | 			account.c_str(), character.c_str(), token);
  1192 | 
  1193 | 		CUser* pUser = g_pMain->GetUserPtr(account, NameType::TYPE_ACCOUNT);
  1194 | 		if (pUser != nullptr)
  1195 | 		{
  1196 | 			pUser->CancelReconnectPendingDisconnect();
  1197 | 			printf("[RECONNECT] stay accepted, pending disconnect cancelled account=%s\n",
  1198 | 				account.c_str());
  1199 | 		}
  1200 | 
  1201 | 		Packet result(XSafe);
  1202 | 		result << uint8(WIZ_RECONNECT) << uint8(7) << uint8(1);
  1203 | 		Send(&result);
  1204 | 		return;
  1205 | 	}
  1206 | 	if (requestType == 8)
  1207 | 	{
  1208 | 		// "Şimdi disconnect": kullanıcı bekleme süresini beklemek istemiyor.
  1209 | 		pkt >> token;
  1210 | 		pkt.SByte();
  1211 | 		pkt >> account >> character;
  1212 | 		printf("[RECONNECT] DISCONNECT-NOW received account=%s character=%s token=%u\n",
  1213 | 			account.c_str(), character.c_str(), token);
  1214 | 
  1215 | 		CUser* pUser = g_pMain->GetUserPtr(account, NameType::TYPE_ACCOUNT);
  1216 | 		if (pUser != nullptr)
  1217 | 		{
  1218 | 			// Askıdaysa askıyı kapat: karakter bilinçli olarak ayrılıyor.
  1219 | 			pUser->ReleaseReconnectSuspend("disconnect now");
  1220 | 
  1221 | 			pUser->CancelReconnectPendingDisconnect();
  1222 | 			// Disconnect'ten ÖNCE bildir: client köprüyü devre dışı bırakıp
  1223 | 			// normal disconnect panelini göstersin.
  1224 | 			pUser->SendReconnectDisconnectNotice();
  1225 | 			pUser->goDisconnect("reconnect test: disconnect requested", __FUNCTION__);
  1226 | 		}
  1227 | 
  1228 | 		Packet result(XSafe);
  1229 | 		result << uint8(WIZ_RECONNECT) << uint8(7) << uint8(1);
  1230 | 		Send(&result);
  1231 | 		return;
  1232 | 	}
  1233 | 	if (requestType != 2)
  1234 | 		return;
  1235 | 
  1236 | 	pkt >> token;
  1237 | 	pkt.SByte();
  1238 | 	pkt >> account >> character;
  1239 | 	printf("[RECONNECT] RESUME received account=%s character=%s token=%u\n",
  1240 | 		account.c_str(), character.c_str(), token);
  1241 | 
  1242 | 	// Premium kapısı (krallık kuralı): reconnect yalnızca premium oyunculara
  1243 | 	// tanınır. Token/ticket geçerli olsa bile - örn. resume isteği yoldayken
  1244 | 	// premium süresi dolduysa - premiumsuz oyuncu yeniden oyuna alınmaz;
  1245 | 	// reddedilir ve Anti-Cheat normal disconnect akışına düşer (oyunun kendi
  1246 | 	// disconnect paneli gelir).
  1247 | 	CUser* pResumeOwner = g_pMain->GetUserPtr(account, NameType::TYPE_ACCOUNT);
  1248 | 	if (pResumeOwner != nullptr && !pResumeOwner->IsReconnectEligible())
  1249 | 	{
  1250 | 		printf("[RECONNECT] RESUME rejected (premium required) account=%s character=%s\n",
  1251 | 			account.c_str(), character.c_str());
  1252 | 		{
  1253 | 			Guard lock(g_reconnectTicketsLock);
  1254 | 			g_reconnectTickets.erase(token);
  1255 | 		}
  1256 | 		Packet result(XSafe);
  1257 | 		result << uint8(WIZ_RECONNECT) << uint8(3) << uint8(0);
  1258 | 		Send(&result);
  1259 | 		return;
  1260 | 	}
  1261 | 
  1262 | 	// Karakter hâlâ oyunda olabilir (disconnect henüz yapılmadı - bekleme süresi
  1263 | 	// içinde "reconnect" seçildi). Bu durumda eski oturumu nazikçe düşür;
  1264 | 	// OnDisconnect ticket'ı disconnected=true ile yeniler ve resume akışı
  1265 | 	// bu (transport) bağlantı üzerinden devam eder.
  1266 | 	bool dropLiveSession = false;
  1267 | 	{
  1268 | 		Guard lock(g_reconnectTicketsLock);
  1269 | 		auto it = g_reconnectTickets.find(token);
  1270 | 		dropLiveSession = it != g_reconnectTickets.end() && !it->second.disconnected;
  1271 | 	}
  1272 | 
  1273 | 	if (dropLiveSession)
  1274 | 	{
  1275 | 		CUser* pOldUser = g_pMain->GetUserPtr(account, NameType::TYPE_ACCOUNT);
  1276 | 		if (pOldUser != nullptr && pOldUser != this)
  1277 | 		{
  1278 | 			printf("[RECONNECT] live session still in game, dropping old connection account=%s character=%s\n",
  1279 | 				account.c_str(), character.c_str());
  1280 | 			pOldUser->goDisconnect("reconnect resume replaced session", __FUNCTION__);
  1281 | 		}
  1282 | 	}
  1283 | 
  1284 | 	bool valid = false;
  1285 | 	ReconnectTicket ticket = {};
  1286 | 	{
  1287 | 		Guard lock(g_reconnectTicketsLock);
  1288 | 		auto it = g_reconnectTickets.find(token);
  1289 | 		if (it != g_reconnectTickets.end())
  1290 | 		{
  1291 | 			valid = it->second.disconnected
  1292 | 				&& it->second.expiresAt >= GetTickCount64()
  1293 | 				&& it->second.account == account
  1294 | 				&& it->second.character == character
  1295 | 				&& (GetAccountName().empty() || GetAccountName() == account);
  1296 | 			if (valid)
  1297 | 			{
  1298 | 				ticket = it->second;
  1299 | 				g_reconnectTickets.erase(it);
  1300 | 			}
  1301 | 		}
  1302 | 	}
  1303 | 
  1304 | 	if (!valid)
  1305 | 	{
  1306 | 		printf("[RECONNECT] RESUME rejected token=%u\n", token);
  1307 | 		Packet result(XSafe);
  1308 | 		result << uint8(WIZ_RECONNECT) << uint8(3) << uint8(0);
  1309 | 		Send(&result);
  1310 | 		return;
  1311 | 	}
  1312 | 
  1313 | 	m_reconnectToken = token;
  1314 | 	if (GetAccountName().empty())
  1315 | 		m_strAccountID = account;
  1316 | 	m_reconnectHydrating = true;
  1317 | 	m_reconnectRestoreGenie = ticket.genieActive;
  1318 | 	m_reconnectMerchantState = ticket.merchantState;
  1319 | 	m_reconnectRestoreZone = ticket.zone;
  1320 | 	m_reconnectRestoreX = ticket.x;
  1321 | 	m_reconnectRestoreY = ticket.y;
  1322 | 	m_reconnectRestoreZ = ticket.z;
  1323 | 	m_reconnectRestoreMerchant = ticket.merchantValid;
  1324 | 	m_reconnectRestorePremiumMerchant = ticket.premiumMerchant;
  1325 | 	memcpy(m_reconnectMerchantItems, ticket.merchantItems, sizeof(m_reconnectMerchantItems));
  1326 | 	// Parti durumu ticket'dan aktarılır; karakter yükleme akışı sonrası
  1327 | 	// RestoreReconnectParty() ile parti slotuna yeniden bağlanır.
  1328 | 	m_reconnectRestorePartyValid = ticket.partyValid;
  1329 | 	m_reconnectRestorePartyID = ticket.partyID;
  1330 | 	m_reconnectRestorePartySlot = ticket.partySlot;
  1331 | 	m_reconnectRestorePartySocket = ticket.partySocket;
  1332 | 	m_reconnectRestorePartyLeader = ticket.partyLeader;
  1333 | 	m_reconnectRestorePartyCommandLeader = ticket.partyCommandLeader;
  1334 | 	m_reconnectRestorePartyType = ticket.partyType;
  1335 | 	printf("[RECONNECT] RESUME accepted account=%s character=%s zone=%u genie=%s merchant=%s party=%s\n",
  1336 | 		account.c_str(), character.c_str(), ticket.zone,
  1337 | 		ticket.genieActive ? "active" : "inactive",
  1338 | 		ticket.merchantValid ? "restore" : "none",
  1339 | 		ticket.partyValid ? "restore" : "none");
  1340 | 
  1341 | 	Packet select(WIZ_SEL_CHAR);
  1342 | 	select << account << character << uint8(1);
  1343 | 	SelCharToAgent(select);
  1344 | }
  1345 | 
  1346 | void CUser::RestoreReconnectMerchant()
  1347 | {
  1348 | 	if (!m_reconnectRestoreMerchant)
  1349 | 		return;
  1350 | 
  1351 | 	memcpy(m_arMerchantItems, m_reconnectMerchantItems, sizeof(m_arMerchantItems));
  1352 | 	m_bPremiumMerchant = m_reconnectRestorePremiumMerchant;
  1353 | 
  1354 | 	if (m_reconnectMerchantState == MERCHANT_STATE_SELLING)
  1355 | 	{
  1356 | 		m_bMerchantState = MERCHANT_STATE_SELLING;
  1357 | 		m_bSellingMerchantPreparing = false;
  1358 | 
  1359 | 		Packet result(WIZ_MERCHANT, uint8(MERCHANT_INSERT));
  1360 | 		result << uint16(0x01) << std::string() << GetSocketID() << m_bPremiumMerchant;
  1361 | 		for (int i = 0; i < MAX_MERCH_ITEMS; ++i)
  1362 | 			result << m_arMerchantItems[i].nNum;
  1363 | 		SendToRegion(&result, nullptr, GetEventRoom());
  1364 | 		MerchantSlipRefList(this, true);
  1365 | 	}
  1366 | 	else if (m_reconnectMerchantState == MERCHANT_STATE_BUYING)
  1367 | 	{
  1368 | 		m_bMerchantState = MERCHANT_STATE_BUYING;
  1369 | 		m_bBuyingMerchantPreparing = false;
  1370 | 
  1371 | 		Packet result(WIZ_MERCHANT, uint8(MERCHANT_BUY_INSERT));
  1372 | 		result << uint8(1);
  1373 | 		Send(&result);
  1374 | 		BuyingMerchantInsertRegion();
  1375 | 	}
  1376 | 
  1377 | 	m_reconnectRestoreMerchant = false;
  1378 | }
  1379 | 
  1380 | // Disconnect sırasında korunan parti üyeliğini yeni oturuma bağlar:
  1381 | //  - eski (ölü) socket girişimiz diğer üyelerin listesinden PARTY_REMOVE ile
  1382 | //    kaldırılır, slot yeni socket'e çevrilir ve PARTY_INSERT ile duyurulur;
  1383 | //  - tarafımıza mevcut parti listesi gönderilir (katılan üye akışıyla aynı).
  1384 | // Çağrı noktası: karakter yükleme akışı (RestoreReconnectMerchant yanındaki
  1385 | // m_reconnectHydrating bloğu).
  1386 | void CUser::RestoreReconnectParty()
  1387 | {
  1388 | 	if (!m_reconnectRestorePartyValid)
  1389 | 		return;
  1390 | 	m_reconnectRestorePartyValid = false;
  1391 | 
  1392 | 	_PARTY_GROUP* pParty = g_pMain->GetPartyPtr(m_reconnectRestorePartyID);
  1393 | 	if (pParty == nullptr)
  1394 | 		return;
  1395 | 
  1396 | 	// Slot çözümü: kayıtlı slot boş ya da hâlâ eski oturumun socket'ini
  1397 | 	// tutuyorsa kullanılır; değilse ilk boş slota düşülür. Yer yoksa
  1398 | 	// geri yükleme yapılmaz (parti bu arada dağılmış/dolmuş olabilir).
  1399 | 	int8 slot = -1;
  1400 | 	if (m_reconnectRestorePartySlot >= 0 && m_reconnectRestorePartySlot < MAX_PARTY_USERS
  1401 | 		&& (pParty->uid[m_reconnectRestorePartySlot] < 0
  1402 | 			|| pParty->uid[m_reconnectRestorePartySlot] == (int16)m_reconnectRestorePartySocket))
  1403 | 	{
  1404 | 		slot = m_reconnectRestorePartySlot;
  1405 | 	}
  1406 | 	else
  1407 | 	{
  1408 | 		for (int i = 0; i < MAX_PARTY_USERS; i++)
  1409 | 		{
  1410 | 			if (pParty->uid[i] < 0 || pParty->uid[i] == (int16)m_reconnectRestorePartySocket)
  1411 | 			{
  1412 | 				slot = (int8)i;
  1413 | 				break;
  1414 | 			}
  1415 | 		}
  1416 | 	}
  1417 | 
  1418 | 	if (slot < 0)
  1419 | 		return;
  1420 | 
  1421 | 	// Diğer üyelerin listesindeki eski (ölü) socket girişimizi kaldır.
  1422 | 	// Parti disconnect'te silinmediği için onlar bizi hâlâ eski socket ile
  1423 | 	// görmeye devam ediyorlar.
  1424 | 	if (m_reconnectRestorePartySocket != 0
  1425 | 		&& m_reconnectRestorePartySocket != (uint16)GetSocketID())
  1426 | 	{
  1427 | 		Packet removeOld(WIZ_PARTY, uint8(PARTY_REMOVE));
  1428 | 		removeOld << m_reconnectRestorePartySocket;
  1429 | 		g_pMain->Send_PartyMember(m_reconnectRestorePartyID, &removeOld);
  1430 | 	}
  1431 | 
  1432 | 	// Yeni oturumu parti slotuna bağla ve tarafımıza ait durumu geri yükle.
  1433 | 	pParty->uid[slot] = GetSocketID();
  1434 | 	m_bInParty = true;
  1435 | 	m_bInEnterParty = true;
  1436 | 	m_sPartyIndex = m_reconnectRestorePartyID;
  1437 | 	m_sUserPartyType = m_reconnectRestorePartyType;
  1438 | 	m_bPartyLeader = m_reconnectRestorePartyLeader;
  1439 | 	m_bPartyCommandLeader = m_reconnectRestorePartyCommandLeader;
  1440 | 
  1441 | 	// Diğer üyelere kendi yeniden katılışımızı duyur (AgreeToJoinTheParty'deki
  1442 | 	// bildirim paketinin birebir aynısı).
  1443 | 	Packet self(WIZ_PARTY, uint8(PARTY_INSERT));
  1444 | 	self << GetSocketID() << uint8(1) << GetName()
  1445 | 		<< m_MaxHp << m_sHp << GetLevel() << GetClass()
  1446 | 		<< m_MaxMp << m_sMp << GetNation() << uint8(0)
  1447 | 		<< pParty->NumberTargetID << m_sUserPartyType
  1448 | 		<< GetLoyaltySymbolRank();
  1449 | 	g_pMain->Send_PartyMember(m_sPartyIndex, &self);
  1450 | 
  1451 | 	// Kendi istemcimize parti listesini gönder (katılan üye akışındaki gibi).
  1452 | 	for (int i = 0; i < MAX_PARTY_USERS; i++)
  1453 | 	{
  1454 | 		if (pParty->uid[i] < 0 || pParty->uid[i] == GetSocketID())
  1455 | 			continue;
  1456 | 
  1457 | 		CUser* pMember = g_pMain->GetUserPtr(pParty->uid[i]);
  1458 | 		if (pMember == nullptr)
  1459 | 			continue;
  1460 | 
  1461 | 		Packet member(WIZ_PARTY, uint8(PARTY_INSERT));
  1462 | 		member << pParty->uid[i]
  1463 | 			<< uint8(1) << pMember->GetName()
  1464 | 			<< pMember->m_MaxHp << pMember->m_sHp << pMember->GetLevel() << pMember->m_sClass
  1465 | 			<< pMember->m_MaxMp << pMember->m_sMp
  1466 | 			<< pMember->GetNation() << uint8(0)
  1467 | 			<< pParty->NumberTargetID << pMember->m_sUserPartyType
  1468 | 			<< pMember->GetLoyaltySymbolRank();
  1469 | 		Send(&member);
  1470 | 	}
  1471 | 
  1472 | 	// Lider sembolü ('P') ve HP değerleri.
  1473 | 	if (m_bPartyLeader)
  1474 | 		StateChangeServerDirect(6, 1);
  1475 | 
  1476 | 	for (int i = 0; i < MAX_PARTY_USERS; i++)
  1477 | 	{
  1478 | 		CUser* pMember = g_pMain->GetUserPtr(pParty->uid[i]);
  1479 | 		if (pMember == nullptr)
  1480 | 			continue;
  1481 | 
  1482 | 		pMember->SendPartyHpManager(PartyType::Send_All);
  1483 | 	}
  1484 | 
  1485 | 	printf("[RECONNECT] PARTY restored account=%s character=%s party=%d slot=%d\n",
  1486 | 		GetAccountName().c_str(), GetName().c_str(),
  1487 | 		(int)m_sPartyIndex, (int)slot);
  1488 | }
  1489 | 
  1490 | // Süresi dolan reconnect ticket'larını temizler. Süresi dolmuş disconnect
  1491 | // ticket'ı artık resume edilemez; o ana kadar partide tutulan eski oturum
  1492 | // slotu da burada serbest bırakılır (aksi halde hayalet üye kalır).
  1493 | // Timer_UpdateSessions tarafından periyodik olarak çağrılır.
  1494 | void SweepReconnectTickets()
  1495 | {
  1496 | 	struct ExpiredTicket
  1497 | 	{
  1498 | 		uint32 token;
  1499 | 		ReconnectTicket ticket;
  1500 | 	};
  1501 | 
  1502 | 	std::vector<ExpiredTicket> expired;
  1503 | 	{
  1504 | 		Guard lock(g_reconnectTicketsLock);
  1505 | 		for (auto it = g_reconnectTickets.begin(); it != g_reconnectTickets.end(); )
  1506 | 		{
  1507 | 			if (it->second.expiresAt >= GetTickCount64())
  1508 | 			{
  1509 | 				++it;
  1510 | 				continue;
  1511 | 			}
  1512 | 
  1513 | 			if (it->second.partyValid)
  1514 | 				expired.push_back({ it->first, it->second });
  1515 | 
  1516 | 			it = g_reconnectTickets.erase(it);
  1517 | 		}
  1518 | 	}
  1519 | 
  1520 | 	for (const auto& entry : expired)
  1521 | 	{
  1522 | 		const ReconnectTicket& ticket = entry.ticket;
  1523 | 		if (ticket.partyID < 0 || ticket.partySlot < 0 || ticket.partySlot >= MAX_PARTY_USERS)
  1524 | 			continue;
  1525 | 
  1526 | 		_PARTY_GROUP* pParty = g_pMain->GetPartyPtr(ticket.partyID);
  1527 | 		if (pParty == nullptr)
  1528 | 			continue;
  1529 | 
  1530 | 		// Yalnızca hâlâ ESKİ oturumun socket'ini tutan slotu serbest bırak
  1531 | 		// (slot bu arada başkasınca kullanılmışsa dokunma).
  1532 | 		if (pParty->uid[ticket.partySlot] != (int16)ticket.partySocket)
  1533 | 			continue;
  1534 | 
  1535 | 		pParty->uid[ticket.partySlot] = -1;
  1536 | 
  1537 | 		Packet result(WIZ_PARTY, uint8(PARTY_REMOVE));
  1538 | 		result << ticket.partySocket;
  1539 | 		g_pMain->Send_PartyMember(ticket.partyID, &result);
  1540 | 
  1541 | 		// Parti tek üyeye düştüyse normal akıştaki gibi dağıt.
  1542 | 		int count = 0;
  1543 | 		for (int i = 0; i < MAX_PARTY_USERS; i++)
  1544 | 			if (pParty->uid[i] >= 0)
  1545 | 				count++;
  1546 | 
  1547 | 		if (count == 1)
  1548 | 		{
  1549 | 			for (int i = 0; i < MAX_PARTY_USERS; i++)
  1550 | 			{
  1551 | 				if (pParty->uid[i] < 0)
  1552 | 					continue;
  1553 | 
  1554 | 				CUser* pRemaining = g_pMain->GetUserPtr(pParty->uid[i]);
  1555 | 				if (pRemaining != nullptr)
  1556 | 					pRemaining->PartyisDelete();
  1557 | 				break;
  1558 | 			}
  1559 | 		}
  1560 | 		else
  1561 | 		{
  1562 | 			for (int i = 0; i < MAX_PARTY_USERS; i++)
  1563 | 			{
  1564 | 				CUser* pMember = g_pMain->GetUserPtr(pParty->uid[i]);
  1565 | 				if (pMember == nullptr)
  1566 | 					continue;
  1567 | 
  1568 | 				pMember->SendPartyHpManager(PartyType::Send_All);
  1569 | 			}
  1570 | 		}
  1571 | 
  1572 | 		printf("[RECONNECT] expired ticket cleaned, party slot released token=%u party=%d slot=%d\n",
  1573 | 			(unsigned)entry.token, (int)ticket.partyID, (int)ticket.partySlot);
  1574 | 	}
  1575 | }
  1576 | std::string PacketSend(uint8 Opcode)
  1577 | {
  1578 | 	std::string PacketHeader;
  1579 | 	switch (Opcode)
  1580 | 	{
  1581 | 	case WIZ_NPC_MOVE:
  1582 | 		return PacketHeader = "WIZ_NPC_MOVE";
  1583 | 		break;
  1584 | 	case WIZ_NPC_INOUT:
  1585 | 		return PacketHeader = "WIZ_NPC_INOUT";
  1586 | 		break;
  1587 | 	case WIZ_MOVE:
  1588 | 		return PacketHeader = "WIZ_MOVE";
  1589 | 		break;
  1590 | 	case WIZ_CHAT:
  1591 | 		return PacketHeader = "WIZ_CHAT";
  1592 | 		break;
  1593 | 	case WIZ_REQ_USERIN:
  1594 | 		return PacketHeader = "WIZ_REQ_USERIN";
  1595 | 		break;
  1596 | 	case WIZ_MYINFO:
  1597 | 		return PacketHeader = "WIZ_MYINFO";

----------------------------------------------------------------------
>> KAYNAK SATIR 2274 - 2329
----------------------------------------------------------------------
  2274 | 		case WIZ_HACKTOOL:
  2275 | 			break;
  2276 | 		case 0xBA:
  2277 | 			break;
  2278 | 		case WIZ_PROGRAMCHECK: // /plc komutunu calıstırır ve kullandıgı programları gösterir sol altta notice olarak 27.09.2020
  2279 | 			PlayerProgramCheck(pkt);
  2280 | 			break;
  2281 | 		case 0xB3:
  2282 | 			break;
  2283 | 		case WIZ_PRESET:	//2370 Skill stat preset paketi
  2284 | 			StatPresetHandle(pkt);
  2285 | 			break;
  2286 | 		case 0x47:
  2287 | 			break;
  2288 | 		default:
  2289 | 			printf("!isInGame [SID=%d] Unknown packet %X AccountName: %s\n", GetSocketID(), command, GetAccountName().c_str());
  2290 | 			return false;
  2291 | 			break;
  2292 | 		}
  2293 | 	}
  2294 | 	else if (isInGame())
  2295 | 	{
  2296 | 		if (command == WIZ_SEL_CHAR)
  2297 | 			return true;
  2298 | 
  2299 | 		// --- Reconnect sistemi: SUSPENDED paket filtresi ------------------
  2300 | 		// Karakter askıdayken (SUSPENDED) hareket, saldırı, yetenek, takas,
  2301 | 		// eşya, NPC, chat... hiçbir paket işlenmez. Yalnızca UI panelinden
  2302 | 		// gelen onay paketi (XSafe/WIZ_RECONNECT) ve asgari yaşam döngüsü
  2303 | 		// paketleri geçer. Paket sessizce yutulur.
  2304 | 		if (isSuspended() && !IsReconnectPacketAllowed(command, pkt))
  2305 | 			return true;
  2306 | 
  2307 | 		bool isCindIn = pCindWar.isEventUser() && g_pMain->isCindirellaZone(GetZoneID());
  2308 | 
  2309 | 		switch (command)
  2310 | 		{
  2311 | 		case WIZ_AKARA:
  2312 | 			OpenAkara(pkt);
  2313 | 			break;
  2314 | 		case XSafe:
  2315 | 			XSafe_HandlePacket(pkt);
  2316 | 			break;
  2317 | 		case WIZ_PROGRAMCHECK: // /plc komutunu calıstırır ve kullandıgı programları gösterir sol altta notice olarak 27.09.2020 
  2318 | 			PlayerProgramCheck(pkt);//tam burada 0x7A vardı değiştirildi
  2319 | 			break;
  2320 | 		case WIZ_ALLCHAR_INFO_REQ:
  2321 | 			AllCharInfo(pkt);
  2322 | 			break;
  2323 | 		case WIZ_GAMESTART:
  2324 | 			GameStart(pkt);
  2325 | 			break;
  2326 | 		case WIZ_SERVER_INDEX:
  2327 | 			SendServerIndex();
  2328 | 			break;
  2329 | 		case WIZ_RENTAL:

